<?
set_time_limit(90);

define("HORIZON_HOST", "horizons.jpl.nasa.gov");
define("HORIZON_PORT", 6775);
define("HORIZON_PROMPT", "Horizons> ");
define("HORIZON_QUERY_PROMPT", "<cr>: ");
define("HORIZON_OPTION_PROMPT", " --> ");
define("HORIZON_CARTESIAN_PROMPT", "? : ");
define("HORIZON_MISC_PROMPT", "] : ");

define("HORIZON_OBSERVE", 0);
define("HORIZON_ELEMENTS", 1);
define("HORIZON_VECTORS", 2);

define("HORIZON_MAJOR_ALL", 0);
define("HORIZON_MAJOR_PLANET", 1);
define("HORIZON_MAJOR_MOON", 2);
define("HORIZON_MAJOR_ARTIFICAL", 3);


header("Content-Type: text/plain; charset=utf-8");

$horz = new HorizonsInterface();

$fn = $_GET["fn"];
switch ($fn) {
	case "major":
		switch ($_GET["type"]) {
			case "planet":
				$type = HORIZON_MAJOR_PLANET;
				break;
			case "moon":
				$type = HORIZON_MAJOR_MOON;
				break;
			case "artificial":
				$type = HORIZON_MAJOR_ARTIFICIAL;
				break;
			case "all":
			default:
				$type = HORIZON_MAJOR_ALL;
				break;
		}

		$r = $horz->get_major($type);
		break;

	//case "minor":
	//	$r = $horz->get_minor();
	//	break;

	case "get":
		$r = $horz->get($_GET["id"]);
		break;

	case "cartesian":
		switch ($_GET["type"]) {
			case "observe":
				$type = HORIZON_OBSERVE;
				break;
			case "elements":
				$type = HORIZON_ELEMENTS;
				break;
			case "vectors":
			default:
				$type = HORIZON_VECTORS;
				break;
		}

		$r = $horz->get_cartesian($_GET["id"], $_GET["from"], $_GET["to"], "500@10", $type);
		break;

	default:
		$r = array();
		break;
}

echo json_encode($r, JSON_PRETTY_PRINT);


// This class is a based on the Python JPL Horizon Ephemeris API project from
// the 2013 Space Apps Challenge.
// https://2013.spaceappschallenge.org/project/python-jpl-horizon-ephemeris-api/
class HorizonsInterface {
	private $telnet;
	private $re_version = '/version|v\s?(\d+\.\d+)/';
	private $re_major = '/^\s{2}(.{7})\s{2}(.{34})\s(.{11})\s{2}(.*)$/m';
	private $re_minor = '/^\s{4}(.{8})\s{2}(.{8})\s{2}(.{13})\s{2}(.{25})\s(.+)$/m';
	private $re_meta = '/(\w+)\s+([\d\/\s]+)$/m';
	private $re_meta_dictA = '/^\s{2}([A-Z].{0,21})=\s{1,2}(.{015})\s[A-Z]/m';
	private $re_meta_dictB = '/\s([A-Z].{0,21})=\s{1,2}(.{0,16})\s?$/m';
	private $re_meta_fail = '/No such object record found\: \d+/';
	private $re_cartesian = '/\$\$SOE(.*)\$\$EOE/s';


	public function __construct() {
		$this->telnet = new Telnet(HORIZON_HOST, HORIZON_PORT, 60);
	}


	private function __open() {
		$this->telnet->connect();
		$this->telnet->readTo(HORIZON_PROMPT);
	}


	private function __close($send_quit = false) {
		if ($send_quit)
			$this->telnet->write("quit");

		$this->telnet->disconnect();
	}


	private function __parse_major($data, $major_type = HORIZON_MAJOR_ALL) {
		preg_match_all($this->re_major, $data, $matches, PREG_SET_ORDER);
		if (count($matches) == 0)
			return [];

		if ($major_type == HORIZON_MAJOR_ALL) {
			$buff = [];
			foreach ($matches as $i => $match) {
				if ($i < 2)
					continue;

				$buff[] = [
					"id" => squeeze($match[1]),
					"name" => squeeze($match[2]),
					"designation" => squeeze($match[3]),
					"alias" => squeeze($match[4]),
				];
			}

			$matches = $buff;
		}
		else {
			$buff = [];
			foreach ($matches as $match) {
				# skip match if id is not an integer
				if (!preg_match('/^\s*-?\d+$/', $match[1]))
					continue;

				$id = (int)$match[1];

				# import pdb; pdb.set_trace()
				if (
					$major_type == HORIZON_MAJOR_ARTIFICAL &&
					($id < 0 || $id > 1000)
				) {
					# print "SATILLITE {0}".format(match[0])
					$buff[] = [
						"id" => squeeze($match[1]),
						"name" => squeeze($match[2]),
						"designation" => squeeze($match[3]),
						"alias" => squeeze($match[4]),
					];
				}
				else if (
					$major_type == HORIZON_MAJOR_PLANET &&
					$id % 100 == 99 && $id > 10
				) {
					# print "PLANET {0}".format(match[0])
					$buff[] = [
						"id" => squeeze($match[1]),
						"name" => squeeze($match[2]),
						"designation" => squeeze($match[3]),
						"alias" => squeeze($match[4]),
					];
				}
				else if (
					$major_type == HORIZON_MAJOR_MOON &&
					$id % 10 != 9 && $id % 10 != 0 && $id > 10 && $id < 1000
				) {
					# print "MOON {0} ({0} % 10 == {1})".format(match[0], id % 10)
					$buff[] = [
						"id" => squeeze($match[1]),
						"name" => squeeze($match[2]),
						"designation" => squeeze($match[3]),
						"alias" => squeeze($match[4]),
					];
				}
				else {
					continue;
				}
			}

			$matches = $buff;
		}

		return $matches;
	}


	private function __parse_minor($data) {
		preg_match_all($this->re_minor, $data, $matches, PREG_SET_ORDER);
		if (count($matches) == 0)
			return [];

		# flip the results
		# matches = list((m[2].lower(), m[0], m[1], m[3]) for m in matches)

		$buff = [];
		foreach ($matches as $match) {
			$buff[] = [
				"id" => squeeze($match[1]),
				"epoch" => squeeze($match[2]),
				"designation" => squeeze($match[3]),
				"name" => squeeze($match[4]),
				"radisu" => squeeze($match[5]),
			];
		}

		# print matches

		return $buff;
	}


	private function __parse_meta($data) {
		if (preg_match($this->re_meta_fail, $data))
			return [ 'error' => 'No such object record found' ];

		$result = [];

		preg_match($this->re_meta, $data, $matches);
		$result["ID"] = explode(" / ", squeeze($matches[2]))[0];
		$result["Name"] = $matches[1];

		preg_match_all($this->re_meta_dictA, $data, $matches, PREG_SET_ORDER);
		foreach ($matches as $match)
			$result[squeeze($match[1])] = squeeze($match[2]);

		preg_match_all($this->re_meta_dictB, $data, $matches, PREG_SET_ORDER);
		foreach ($matches as $match)
			$result[squeeze($match[1])] = squeeze($match[2]);

		return $result;
	}


	private function __parse_cartesian($data) {
		preg_match($this->re_cartesian, $data, $matches);

		$result = [];

		$lines = explode("\r\n", squeeze($matches[1]));
		for ($l = 0; $l < count($lines); $l +=4 ) {
			$vector = [];
			for ($i = 0; $i < 4; $i++) {
				$key = "";
				if ($i % 4 == 0) {
					$key = "time";
				}
				else if ($i % 4 == 3) {
					$key = "ltrgrr";
				}
				else if ($i % 4 == 2) {
					$key = "vxvyvz";
				}
				else if ($i % 4 == 1) {
					$key = "xyz";
				}
				else {
					break;
				}
				$vector[$key] = squeeze($lines[$l + $i]);
			}
			$result[] = $vector;
		}

		return $result;
	}


	private function __parse_version($data) {
		preg_match($this->re_version, $data, $matches);
		if (count($matches) == 0)
			return 0;
		# fail if cant find
		return $matches[1];
	}


	// =================
	// get_major ( self, type ) : returns [{},]
	// =================
	//
	// type: horizon.interface.HORIZON_MAJOR_ALL (default)
	//       horizon.interface.HORIZON_MAJOR_ARTIFICAL
	//       horizon.interface.HORIZON_MAJOR_MOON
	//       horizon.interface.HORIZON_MAJOR_PLANET
	//
	// Returns a list of major bodies and corresponding IDs
	public function get_major($type = HORIZON_MAJOR_ALL) {
		$this->__open();
		$this->telnet->write("MB");

		$result = $this->telnet->readTo(HORIZON_QUERY_PROMPT);

		$this->__close(true);

		return $this->__parse_major($result, $type);
	}


	// =================
	// get_minor ( self, page ) : returns [{},]
	// =================
	//
	// page: (default: 0) integer to specify page in pagination
	//
	// page_size (default: -1): integer to specify how many
	//     results per page
	//
	// Returns a list of minor bodies and corresponding IDs
	public function get_minor($page = 0, $page_size = -1) {
		$this->__open();
		$this->telnet->write("RAD > 0");

		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		# print buff

		$this->telnet->write("");
		$result = $this->telnet->readTo(HORIZON_OPTION_PROMPT);
		# print result

		$this->__close(true);

		return $this->__parse_minor($result);
	}


	public function get_cartesian($id, $start = null, $end = null, $ref = "500@10", $type = HORIZON_VECTORS, $frequency = "1h") {
		if ($start == null)
			$start = gmdate("Y-m-d H:i:s", time() - 60);
		if ($end == null)
			$end = gmdate("Y-m-d H:i:s");

		# make sure start / end are valid or throw error

		$this->__open();
		$this->telnet->write("$id");

		$this->telnet->readTo(HORIZON_QUERY_PROMPT);
		$this->telnet->write("E");

		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		# select cartesian data type
		if ($type == HORIZON_OBSERVE)
			$this->telnet->write("o");
		else if ($type == HORIZON_ELEMENTS)
			$this->telnet->write("e");
		else
			$this->telnet->write("v");

		# select reference point ID, coord, geo
		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("$ref");

		$this->telnet->readTo(HORIZON_OPTION_PROMPT);
		$this->telnet->write("y");

		# select reference plane: body, eclip, frame
		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("body");

		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("$start");
		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("$end");

		# frequency: 1d, 1h, 1m
		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("$frequency");

		# accept output
		$this->telnet->readTo(HORIZON_MISC_PROMPT);
		$this->telnet->write("y");

		$result = $this->telnet->readTo(HORIZON_CARTESIAN_PROMPT);

		# Cartesian queries have a weird exit
		$this->telnet->write("N");
		$this->__close(true);

		return $this->__parse_cartesian($result);
	}


	/*
	public function query($query) {
		$this->__open();
		$this->telnet->write("$query");

		$result = $this->telnet->readTo(HORIZON_QUERY_PROMPT);

		$this->__close(true);

		if (contains($result, "Number of matches"))
			return $this->__parse_list($result);

		# if query returns single result, use meta parser
		return $this->__parse_meta($result);
	}
	*/


	public function get($id) {
		$this->__open();
		$this->telnet->write("$id");

		$result = $this->telnet->readTo(HORIZON_QUERY_PROMPT);
		if ($id < 0) {
			echo "$result\n";
			$this->__close(true);
			return null;
		}

		$this->__close(true);

		return $this->__parse_meta($result);
	}


	public function get_version() {
		$this->__open();
		$this->telnet->write("quit");

		$result = $this->telnet->readTo(HORIZON_QUERY_PROMPT);

		$this->__close();

		return $this->__parse_version($result);
	}
}


/**
 * Telnet class
 *
 * Used to execute remote commands via telnet connection
 * Usess sockets functions and fgetc() to process result
 *
 * All methods throw Exceptions on error
 *
 * Written by Dalibor Andzakovic <dali@swerve.co.nz>
 * Based on the code originally written by Marc Ennaji and extended by
 * Matthias Blaser <mb@adfinis.ch>
 *
 * Extended by Christian Hammers <chammers@netcologne.de>
 * Modified by Frederik Sauer <fsa@dwarf.dk>
 *
 * Modified by Jay Cain <jay@audeamus.info>
 * https://github.com/ngharo/Random-PHP-Classes/blob/master/Telnet.class.php
 */
class Telnet {

	private $host;
	private $port;
	private $timeout;
	private $stream_timeout_sec;
	private $stream_timeout_usec;

	private $socket = NULL;
	private $buffer = NULL;
	private $errno;
	private $errstr;

	private $NULL;
	private $DC1;
	private $WILL;
	private $WONT;
	private $DO;
	private $DONT;
	private $IAC;

	const TELNET_ERROR = FALSE;
	const TELNET_OK = TRUE;

	/**
	 * Constructor. Initialises host, port and timeout parameters
	 * defaults to localhost port 23 (standard telnet port)
	 *
	 * @param string $host Host name or IP addres
	 * @param int $port TCP port number
	 * @param int $timeout Connection timeout in seconds
	 * @param string $prompt Telnet prompt string
	 * @param float $streamTimeout Stream timeout in decimal seconds
	 * @return void
	 */
	public function __construct($host = '127.0.0.1', $port = '23', $timeout = 10, $stream_timeout = 3) {
		$this->host = $host;
		$this->port = $port;
		$this->timeout = $timeout;
		$this->setStreamTimeout($stream_timeout);

		// set some telnet special characters
		$this->NULL = chr(0);
		$this->DC1 = chr(17);
		$this->WILL = chr(251);
		$this->WONT = chr(252);
		$this->DO = chr(253);
		$this->DONT = chr(254);
		$this->IAC = chr(255);
	}

	/**
	 * Destructor. Cleans up socket connection and command buffer
	 *
	 * @return void
	 */
	public function __destruct() {
		// clean up resources
		$this->disconnect();
		$this->buffer = NULL;
	}

	/**
	 * Attempts connection to remote host. Returns TRUE if successful.
	 *
	 * @return boolean
	 */
	public function connect() {
		// check if we need to convert host to IP
		if (!preg_match('/([0-9]{1,3}\\.){3,3}[0-9]{1,3}/', $this->host)) {
			$ip = gethostbyname($this->host);

			if ($this->host == $ip) {
				throw new Exception("Cannot resolve $this->host");
			} else {
				$this->host = $ip;
			}
		}

		// attempt connection - suppress warnings
		$this->socket = @fsockopen($this->host, $this->port, $this->errno, $this->errstr, $this->timeout);

		if (!$this->socket) {
			throw new Exception("Cannot connect to $this->host on port $this->port");
		}

		return self::TELNET_OK;
	}

	/**
	 * Closes IP socket
	 *
	 * @return boolean
	 */
	public function disconnect() {
		if ($this->socket) {
			if (! fclose($this->socket)) {
				throw new Exception("Error while closing telnet socket");
			}
			$this->socket = NULL;
		}
		return self::TELNET_OK;
	}

	/**
	 * Sets the stream timeout.
	 *
	 * @param float $timeout
	 * @return void
	 */
	public function setStreamTimeout($timeout) {
		$this->stream_timeout_usec = (int)(fmod($timeout, 1) * 1000000);
		$this->stream_timeout_sec = (int)$timeout;
	}

	/**
	 * Gets character from the socket
	 *
	 * @return void
	 */
	protected function getc() {
		stream_set_timeout($this->socket, $this->stream_timeout_sec, $this->stream_timeout_usec);
		$c = fgetc($this->socket);
		return $c;
	}

	/**
	 * Clears internal command buffer
	 *
	 * @return void
	 */
	public function clearBuffer() {
		$this->buffer = '';
	}

	/**
	 * Reads characters from the socket and adds them to command buffer.
	 * Handles telnet control characters. Stops when prompt is ecountered.
	 *
	 * @param string $prompt
	 * @return boolean
	 */
	public function readTo($prompt) {
		if (!$this->socket) {
			throw new Exception("Telnet connection closed");
		}

		//$prompt = preg_quote($str, '/');

		// clear the buffer
		$this->clearBuffer();

		$until_t = time() + $this->timeout;
		do {
			// time's up (loop can be exited at end or through continue!)
			if (time() > $until_t) {
				throw new Exception("Couldn't find the requested : '$prompt' within {$this->timeout} seconds");
			}

			$c = $this->getc();

			if ($c === FALSE) {
				if (empty($prompt)) {
					return $this->buffer;
				}
				throw new Exception("Couldn't find the requested : '" . $prompt . "', it was not in the data returned from server: " . $this->buffer);
			}

			// Interpreted As Command
			if ($c == $this->IAC) {
				if ($this->negotiateTelnetOptions()) {
					continue;
				}
			}

			// append current char to global buffer
			$this->buffer .= $c;

			// we've encountered the prompt. Break out of the loop
			if (!empty($prompt) && endsWith($this->buffer, $prompt)) {// preg_match("/{$prompt}$/", $this->buffer)) {
				return $this->buffer;
			}

		} while ($c != $this->NULL && $c != $this->DC1);
	}

	/**
	 * Write command to a socket
	 *
	 * @param string $buffer Stuff to write to socket
	 * @param boolean $add_newline Default TRUE, adds newline to the command
	 * @return boolean
	 */
	public function write($buffer, $add_newline = TRUE) {
		if (!$this->socket) {
			throw new Exception("Telnet connection closed");
		}

		// clear buffer from last command
		$this->clearBuffer();

		if ($add_newline == TRUE) {
			$buffer .= "\n";
		}

		if (!fwrite($this->socket, $buffer) < 0) {
			throw new Exception("Error writing to socket");
		}
		fflush($this->socket);

		return self::TELNET_OK;
	}

	/**
	 * Returns the content of the command buffer
	 *
	 * @return string Content of the command buffer
	 */
	protected function getBuffer() {
		// Remove all carriage returns from line breaks
		$buf =  preg_replace('/\r\n|\r/', "\n", $this->buffer);
		// Cut last line from buffer (almost always prompt)
		if ($this->strip_prompt) {
			$buf = explode("\n", $buf);
			unset($buf[count($buf) - 1]);
			$buf = implode("\n", $buf);
		}
		return trim($buf);
	}

	/**
	 * Telnet control character magic
	 *
	 * @param string $command Character to check
	 * @return boolean
	 */
	protected function negotiateTelnetOptions() {
		$c = $this->getc();

		if (($c == $this->DO) || ($c == $this->DONT)) {
			$opt = $this->getc();
			fwrite($this->socket, $this->IAC . $this->WONT . $opt);
		} else if (($c == $this->WILL) || ($c == $this->WONT)) {
			$opt = $this->getc();
			fwrite($this->socket, $this->IAC . $this->DONT . $opt);
		} else {
			throw new Exception('Error: unknown control character ' . ord($c));
		}

		return self::TELNET_OK;
	}
}


function contains($str, $frag) {
	return (strpos($str, $frag) !== false);
}


/// Checks whether the first part of one string is equal to another string.
function startsWith($str, $frag) {
	return (strpos($str, $frag) === 0);
}


/// Checks whether the last part of one string is equal to another string.
function endsWith($str, $frag) {
	$index = strlen($str) - strlen($frag);
	return (strrpos($str, $frag) === $index);
}


function squeeze($str) {
	return preg_replace('/\h{2,}/u', " ", trim($str));
}
?>